<?php
/**
* @version    $Id: simplexml.php,v 1.4.4.1 2008/08/06 13:41:27 islandusurper Exp $
* @package    Joomla.Framework
* @subpackage Utilities
* @copyright  Copyright (C) 2005 - 2007 Open Source Matters. All rights reserved.
* @license    GNU/GPL, see LICENSE.php
* Joomla! is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*/

/**
 * SimpleXML implementation.
 *
 * The XML Parser extension (expat) is required to use JSimpleXML.
 *
 * The class provides a pure PHP4 implementation of the PHP5
 * interface SimpleXML. As with PHP5's SimpleXML it is what it says:
 * simple. Nevertheless, it is an easy way to deal with XML data,
 * especially for read only access.
 *
 * Because it's not possible to use the PHP5 ArrayIterator interface
 * with PHP4 there are some differences between this implementation
 * and that of PHP5:
 *
 * <ul>
 * <li>The access to the root node has to be explicit in
 * JSimpleXML, not implicit as with PHP5. Write
 * $xml->document->node instead of $xml->node</li>
 * <li>You cannot acces CDATA using array syntax. Use the method data() instead</li>
 * <li>You cannot access attributes directly with array syntax. use attributes()
 * to read them.</li>
 * <li>Comments are ignored.</li>
 * <li>Last and least, this is not as fast as PHP5 SimpleXML--it is pure PHP4.</li>
 * </ul>
 *
 * Example:
 * <code>
 * :simple.xml:
 * <?xml version="1.0" encoding="utf-8" standalon="yes"?>
 * <document>
 *   <node>
 *   <child gender="m">Tom Foo</child>
 *   <child gender="f">Tamara Bar</child>
 *   <node>
 * </document>
 *
 * ---
 *
 * // read and write a document
 * $xml = new JSimpleXML;
 * $xml->loadFile('simple.xml');
 * print $xml->asXML();
 *
 * // access a given node's CDATA
 * print $xml->root->node->child[0]->data(); // Tom Foo
 *
 * // access attributes
 * $attr = $xml->root->node->child[1]->attributes();
 * print $attr['gender']; // f
 *
 * // access children
 * foreach( $xml->root->node->children() as $child ) {
 *   print $child->data();
 * }
 * </code>
 *
 * Note: JSimpleXML cannot be used to access sophisticated XML doctypes
 * using datatype ANY (e.g. XHTML). With a DOM implementation you can
 * handle this.
 *
 *
 * @author    Johan Janssens <johan.janssens@joomla.org>
 * @package   Joomla.Framework
 * @subpackage  Utilities
 * @since 1.5
 */
class JSimpleXML {
  /**
   * The XML parser
   *
   * @var resource
   */
  var $_parser = null;

  /**
  * The XML document
  *
  * @var string
  */
  var $_xml = '';

  /**
  * Document element
  *
  * @var object
  */
  var $document = null;

  /**
  * Current object depth
  *
  * @var array
  */
  var $_stack = array();


  /**
   * A hack to support __construct() on PHP 4
   *
   * Hint: descendant classes have no PHP4 class_name() constructors,
   * so this constructor gets called first and calls the top-layer __construct()
   * which (if present) should call parent::__construct()
   *
   * @access  public
   * @return  Object
   * @since 1.5
   */
  function JSimpleXML()
  {
    $args = func_get_args();
    call_user_func_array(array(&$this, '__construct'), $args);
  }

  /**
   * Constructor.
   *
   * @access protected
   */
  function __construct($options = null)
  {
    if(! function_exists('xml_parser_create')) {
      return false; //TODO throw warning
    }

    //Create the parser resource and make sure both versions of PHP autodetect the format.
    $this->_parser = xml_parser_create('');

    // check parser resource
    xml_set_object($this->_parser, $this);
    xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, 0);
    if( is_array($options) )
    {
      foreach( $options as $option => $value ) {
        xml_parser_set_option($this->_parser, $option, $value);
      }
    }

    //Set the handlers
    xml_set_element_handler($this->_parser, '_startElement', '_endElement');
    xml_set_character_data_handler($this->_parser, '_characterData');
  }

   /**
   * Interprets a string of XML into an object
   *
   * This function will take the well-formed xml string data and return an object of class
   * JSimpleXMLElement with properties containing the data held within the xml document.
   * If any errors occur, it returns FALSE.
   *
   * @param string  Well-formed xml string data
   * @param string  currently ignored
   * @return object JSimpleXMLElement
   */
  function loadString($string, $classname = null) {
    $this->_parse($string);
    return true;
  }

   /**
   * Interprets an XML file into an object
   *
   * This function will convert the well-formed XML document in the file specified by filename
   * to an object  of class JSimpleXMLElement. If any errors occur during file access or
   * interpretation, the function returns FALSE.
   *
   * @param string  Path to xml file containing a well-formed XML document
   * @param string  currently ignored
   * @return boolean True if successful, false if file empty
   */
  function loadFile($path, $classname = null)
  {
    //Get the XML document loaded into a variable
    $xml = trim( file_get_contents($path) );
    if ($xml == '')
    {
      return false;
    }
    else
    {
      $this->_parse($xml);
      return true;
    }
  }

  /**
   * Get a JSimpleXMLElement object from a DOM node.
   *
   * This function takes a node of a DOM  document and makes it into a JSimpleXML node.
   * This new object can then be used as a native JSimpleXML element. If any errors occur,
   * it returns FALSE.
   *
   * @param string  DOM  document
   * @param string    currently ignored
   * @return object   JSimpleXMLElement
   */
  function importDom($node, $classname = null) {
    return false;
  }

  /**
   * Get the parser
   *
   * @access public
   * @return resource XML parser resource handle
   */
  function getParser() {
    return $this->_parser;
  }

  /**
   * Set the parser
   *
   * @access public
   * @param resource  XML parser resource handle
   */
  function setParser($parser) {
    $this->_parser = $parser;
  }

  /**
   * Start parsing an XML document
   *
   * Parses an XML document. The handlers for the configured events are called as many times as necessary.
   *
   * @param $xml  string  data to parse
   */
  function _parse($data = '')
  {
    //Error handling
    if (!xml_parse($this->_parser, $data)) {
      $this->_handleError(
        xml_get_error_code($this->_parser),
        xml_get_current_line_number($this->_parser),
        xml_get_current_column_number($this->_parser)
      );
    }

    //Free the parser
    xml_parser_free($this->_parser);
  }

  /**
   * Handles an XML parsing error
   *
   * @access protected
   * @param int $code XML Error Code
   * @param int $line Line on which the error happened
   * @param int $col Column on which the error happened
   */
  function _handleError($code, $line, $col)
  {
    watchdog('JSimpleXML', 'XML Parsing Error at '.$line.':'.$col.'. Error '.$code.': '.xml_error_string($code), WATCHDOG_ERROR);
  }

  /**
   * Gets the reference to the current direct parent
   *
   * @return object
   */
  function _getStackLocation()
  {
    $return = '';
    foreach($this->_stack as $stack) {
      $return .= $stack.'->';
    }

    return rtrim($return, '->');
  }

  /**
   * Handler function for the start of a tag
   *
   * @access protected
   * @param resource $parser
   * @param string $name
   * @param array $attrs
   */
  function _startElement($parser, $name, $attrs = array())
  {
    //Make the name of the tag lower case
    $name = strtolower($name);

    //Check to see if tag is root-level
    if (count($this->_stack) == 0)
    {
      //If so, set the document as the current tag
      $this->document = new JSimpleXMLElement($name, $attrs);

      //And start out the stack with the document tag
      $this->_stack = array('document');
    }
    //If it isn't root level, use the stack to find the parent
    else
    {
       //Get the name which points to the current direct parent, relative to $this
      $parent = $this->_getStackLocation();

      //Add the child
      eval('$this->'.$parent.'->addChild($name, $attrs, '.count($this->_stack).');');

      //Update the stack
      eval('$this->_stack[] = $name.\'[\'.(count($this->'.$parent.'->'.$name.') - 1).\']\';');
    }
  }

  /**
   * Handler function for the end of a tag
   *
   * @access protected
   * @param resource $parser
   * @param string $name
   */
  function _endElement($parser, $name)
  {
    //Update stack by removing the end value from it as the parent
    array_pop($this->_stack);
  }

  /**
   * Handler function for the character data within a tag
   *
   * @access protected
   * @param resource $parser
   * @param string $data
   */
  function _characterData($parser, $data)
  {
    //Get the reference to the current parent object
    $tag = $this->_getStackLocation();

    //Assign data to it
    eval('$this->'.$tag.'->_data .= trim($data);');
  }
}


/**
 * SimpleXML Element
 *
 * This object stores all of the direct children of itself in the $children array.
 * They are also stored by type as arrays. So, if, for example, this tag had 2 <font>
 * tags as children, there would be a class member called $font created as an array.
 * $font[0] would be the first font tag, and $font[1] would be the second.
 *
 * To loop through all of the direct children of this object, the $children member
 *  should be used.
 *
 * To loop through all of the direct children of a specific tag for this object, it
 * is probably easier to use the arrays of the specific tag names, as explained above.
 *
 * @author    Johan Janssens <johan.janssens@joomla.org>
 * @package   Joomla.Framework
 * @subpackage  Utilities
 * @since 1.5
 */
class JSimpleXMLElement {
  /**
   * Array with the attributes of this XML element
   *
   * @var array
   */
  var $_attributes = array();

  /**
   * The name of the element
   *
   * @var string
   */
  var $_name = '';

  /**
   * The data the element contains
   *
   * @var string
   */
  var $_data = '';

  /**
   * Array of references to the objects of all direct children of this XML object
   *
   * @var array
   */
  var $_children = array();

  /**
   * The level of this XML element
   *
   * @var int
   */
  var $_level = 0;

  /**
   * A hack to support __construct() on PHP 4
   *
   * Hint: descendant classes have no PHP4 class_name() constructors,
   * so this constructor gets called first and calls the top-layer __construct()
   * which (if present) should call parent::__construct()
   *
   * @access  public
   * @return  Object
   * @since 1.5
   */
  function JSimpleXMLElement()
  {
    $args = func_get_args();
    call_user_func_array(array(&$this, '__construct'), $args);
  }

  /**
   * Constructor, sets up all the default values
   *
   * @param string $name
   * @param array $attrs
   * @param int $parents
   * @return JSimpleXMLElement
   */
  function __construct($name, $attrs = array(), $level = 0)
  {
    //Make the keys of the attr array lower case, and store the value
    $this->_attributes = array_change_key_case($attrs, CASE_LOWER);

    //Make the name lower case and store the value
    $this->_name = strtolower($name);

    //Set the level
    $this->_level = $level;
  }

  /**
   * Get the name of the element
   *
   * @access public
   * @return string
   */
  function name() {
    return $this->_name;
  }

  /**
   * Get the an attribute of the element
   *
   * @param string $attribute   The name of the attribute
   *
   * @access public
   * @return mixed If an attribute is given will return the attribute if it exist.
   *         If no attribute is given will return the complete attributes array
   */
  function attributes($attribute = null)
  {
    if(!isset($attribute)) {
      return $this->_attributes;
    }

    return isset($this->_attributes[$attribute]) ? $this->_attributes[$attribute] : null;
  }

  /**
   * Get the data of the element
   *
   * @access public
   * @return string
   */
  function data() {
    return $this->_data;
  }

  /**
   * Set the data of the element
   *
   * @access public
   * @param string $data
   * @return string
   */
  function setData($data) {
    $this->_data = $data;
  }

  /**
   * Get the children of the element
   *
   * @access public
   * @return array
   */
  function children() {
    return $this->_children;
  }

  /**
   * Get the level of the element
   *
   * @access public
   * @return int
   */
  function level() {
    return $this->_level;
  }

   /**
   * Adds an attribute to the element
   *
   * @param string $name
   * @param array  $attrs
   */
  function addAttribute($name, $value)
  {
    //add the attribute to the element, override if it already exists
    $this->_attributes[$name] = $value;
  }

   /**
   * Removes an attribute from the element
   *
   * @param string $name
   */
  function removeAttribute($name)
  {
    unset($this->_attributes[$name]);
  }

  /**
   * Adds a direct child to the element
   *
   * @param string $name
   * @param array  $attrs
   * @param int    $level
   */
  function addChild($name, $attrs, $level)
  {
    //If there is no array already set for the tag name being added,
    //create an empty array for it
    if(!isset($this->$name))
      $this->$name = array();

    //Create the child object itself
    $child = new JSimpleXMLElement($name, $attrs, $level);

    //Add the reference of it to the end of an array member named for the elements name
    $this->{$name}[] =& $child;

    //Add the reference to the children array member
    $this->_children[] =& $child;
  }

  function removeChild(&$child)
  {
    $name = $child->name();
    for ($i=0,$n=count($this->_children);$i<$n;$i++)
    {
      if ($this->_children[$i] == $child) {
        unset($this->_children[$i]);
      }
    }
    for ($i=0,$n=count($this->{$name});$i<$n;$i++)
    {
      if ($this->{$name}[$i] == $child) {
        unset($this->{$name}[$i]);
      }
    }
    $this->_children = array_values($this->_children);
    $this->{$name} = array_values($this->{$name});
    unset($child);
  }

  /**
   * Get an element in the document by / separated path
   *
   * @param string  $path The / separated path to the element
   * @return  object  JSimpleXMLElement
   */
  function &getElementByPath($path)
  {
    $tmp  =& $this;
    $false  = false;
    $parts  = explode('/', trim($path, '/'));

    foreach ($parts as $node)
    {
      $found = false;
      foreach ($tmp->_children as $child)
      {
        if ($child->_name == $node)
        {
          $tmp =& $child;
          $found = true;
          break;
        }
      }
      if (!$found) {
        break;
      }
    }

    if ($found) {
      $ref =& $tmp;
    } else {
      $ref =& $false;
    }
    return $ref;
  }

  function map($callback, $args=array())
  {
    $callback($this, $args);
    // Map to all children
    if ($n = count($this->_children)) {
      for($i=0;$i<$n;$i++)
      {
        $this->_children[$i]->map($callback, $args);
      }
    }
  }

  /**
   * Return a well-formed XML string based on SimpleXML element
   *
   * @return string
   */
  function asXML($whitespace=true)
  {
    //Start a new line, indent by the number indicated in $this->level, add a <, and add the name of the tag
    if ($whitespace) {
      $out = "\n".str_repeat("\t", $this->_level).'<'.$this->_name;
    } else {
      $out = '<'.$this->_name;
    }

    //For each attribute, add attr="value"
    foreach($this->_attributes as $attr => $value)
      $out .= ' '.$attr.'="'.$value.'"';

    //If there are no children and it contains no data, end it off with a />
    if(empty($this->_children) && empty($this->_data))
      $out .= " />";

    //Otherwise...
    else
    {
      //If there are children
      if(!empty($this->_children))
      {
        //Close off the start tag
        $out .= '>';

        //For each child, call the asXML function (this will ensure that all children are added recursively)
        foreach($this->_children as $child)
          $out .= $child->asXML($whitespace);

        //Add the newline and indentation to go along with the close tag
        if ($whitespace) {
          $out .= "\n".str_repeat("\t", $this->_level);
        }
      }

      //If there is data, close off the start tag and add the data
      elseif(!empty($this->_data))
        $out .= '>'.$this->_data;

      //Add the end tag
      $out .= '</'.$this->_name.'>';
    }

    //Return the final output
    return $out;
  }
}